package jtacck.labmedicine;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.concurrent.*;

import jvm.jtacck.db.records.CheckListPair;
import jvm.jtacck.db.records.ClientSite;
import jvm.jtacck.serials.Barcode;
import jvm.jtacck.serials.GPS;
import jvm.jtacck.serials.LoginInfo;
import jvm.jtacck.serials.Route;
import jvm.jtacck.serials.ShippingList;
import android.util.Log;

/**
 * @author Jesse Jeng
 * 
 *         A framework used by the Android platform to connect to the Android
 *         server through sockets. Also handles asynchronous updates of routes
 *         and GPS coordinates
 * 
 */
public class ServerLinker {
    private InetAddress hostAddress;
    private Socket listenSocket;
    private Socket requestSocket;
    private int listenPort;
    private int requestPort;
    private LoginInfo login;
    private boolean authenticated;
    private Route nextRoute;
    private CheckListPair clp;
    private ObjectOutputStream lsoos;
    private ObjectInputStream lsois;
    private ObjectOutputStream rsoos;
    private ObjectInputStream rsois;
    private BlockingQueue<GPS> gpsQueue;
    private volatile boolean gpsThreading;
    private GPSThread gpsThread;
    private ListenThread listenThread;
    private boolean loggedIn;
    private boolean hasNextRoute;

    /**
     * @param hostAddress
     *            Network address of the Android server
     * @param listenPort
     *            Port number for listening
     * @param requestPort
     *            Port number for requesting
     * @param login
     *            LoginInfo that will be sent to the server
     */
    public ServerLinker(String hostAddress, int listenPort, int requestPort,
            LoginInfo login) {
        try {
            this.hostAddress = InetAddress.getByName(hostAddress);
        } catch (UnknownHostException e) {
            Log.w("AndroidServer", "Unknown host");
        }
        this.listenPort = listenPort;
        this.requestPort = requestPort;
        this.listenSocket = null;
        this.requestSocket = null;
        if (login.username == null) {
            login = new LoginInfo("", "");
        }
        this.login = login;
        this.authenticated = false;
        this.nextRoute = null;
        this.clp = null;
        this.gpsQueue = new LinkedBlockingQueue<GPS>();
        this.gpsThread = null;
        this.listenThread = null;
        this.loggedIn = false;
        this.hasNextRoute = false;
    }

    /**
     * Connects to the Android server by opening sockets
     * 
     * @return True if a successful connection was made to the Android server
     */
    public boolean connect() {
        try {
            if (listenSocket == null || !listenSocket.isConnected()) {
                listenSocket = new Socket(hostAddress, listenPort);
            }
            if (requestSocket == null || !requestSocket.isConnected()) {
                requestSocket = new Socket(hostAddress, requestPort);
            }
        } catch (Exception e) {
            authenticated = false;
            Log.w("AndroidServer", "Can't connect to sockets");
            return false;
        }
        return true;
    }

    /**
     * Disconnects from the server and closes all IO streams
     */
    public void disconnect() {
        try {
            if (!listenSocket.isOutputShutdown()) {
                listenSocket.shutdownOutput();
            }
            if (!listenSocket.isInputShutdown()) {
                listenSocket.shutdownInput();
            }
            if (!listenSocket.isClosed()) {
                listenSocket.close();
            }
        } catch (Exception e) {
            Log.w("AndroidServer", "Error closing listen socket");
        }
        try {
            if (!requestSocket.isOutputShutdown()) {
                requestSocket.shutdownOutput();
            }
            if (!requestSocket.isInputShutdown()) {
                requestSocket.shutdownInput();
            }
            if (!requestSocket.isClosed()) {
                requestSocket.close();
            }
        } catch (Exception e) {
            Log.w("AndroidServer", "Error closing request socket");
        }
        gpsThreading = false;
        if (gpsThread != null) {
            gpsThread.interrupt();
        }
        if (listenThread != null) {
            listenThread.interrupt();
        }
        authenticated = false;
    }

    public void startListening() {
        if (listenThread == null) {
            listenThread = new ListenThread();
            listenThread.start();
        }
    }

    public boolean hasCheckListPair() {
        return clp != null;
    }

    public CheckListPair getCheckListPair() {
        if (hasCheckListPair()) {
            return clp;
        } else {
            try {
                synchronized (listenThread) {
                    listenThread.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return clp;
        }
    }

    public boolean hasNextRoute() {
        return hasNextRoute;
    }

    public Route getNextRoute() {
        if (nextRoute != null) {
            hasNextRoute = false;
            return nextRoute;
        } else {
            try {
                synchronized (listenThread) {
                    listenThread.wait();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            hasNextRoute = false;
            return nextRoute;
        }
    }

    private synchronized Object request(Object message) {
        if (!authenticated) {
            return null;
        }
        if (message == null) {
            return null;
        }
        RequestThread rt = new RequestThread(message);
        rt.start();
        try {
            rt.join();
        } catch (InterruptedException e) {
            // Do something
        }
        return rt.getInput();
    }

    /**
     * @param barcode
     *            Barcode of the package that was collected
     * @return True if the status of the package was updated successfully
     */
    public boolean packageCollected(String barcode) {
        // TODO: Implement this method
        Object result = request(new Barcode(barcode));
        return result != null && !(result instanceof Exception);
    }

    /**
     * Updates the server/database of the phone/courier's current GPS location
     * 
     * @param latitude
     *            GPS latitude coordinate
     * @param longitude
     *            GPS longitude coordinate
     */
    public void updateGPS(double latitude, double longitude) {
        // GPS coordinate = new GPS(latitude, longitude);
        // Object result = request(coordinate);
        // return result != null && !(result instanceof Exception);
        gpsQueue.offer(new GPS(latitude, longitude));
    }

    /**
     * @param Current
     *            pickup location
     * @return A list of shipments/packages to be scanned/picked up
     */
    public ShippingList getShippingList(ClientSite site) {
        Log.w("DEBUG", "HERE");
        Object result = request(site);
        if (result != null && !(result instanceof Exception)) {
            return (ShippingList) result;
        }
        return null;
    }

    /**
     * @return True if the user is authenticated
     */
    public boolean authenticated() {
        return authenticated;
    }

    /**
     * @return True if the user already logged in
     */
    public boolean loggedIn() {
        return loggedIn;
    }

    /**
     * Authenticates the phone against the Android server by sending login
     * information that contains username and password
     */
    public void authenticate() {
        try {
            if (!authenticated && connect()) {
                Object input = null;
                lsoos = new ObjectOutputStream(listenSocket.getOutputStream());
                lsoos.flush();
                lsois = new ObjectInputStream(listenSocket.getInputStream());
                lsoos.writeObject(login);
                input = lsois.readObject();
                authenticated = !(input instanceof Exception);
                if (!authenticated) {
                    loggedIn = ((Exception) input).getMessage().compareTo(
                            "Already logged in") == 0;
                }

                // Authenticate the socket on requestPort
                rsoos = new ObjectOutputStream(requestSocket.getOutputStream());
                rsoos.flush();
                rsois = new ObjectInputStream(requestSocket.getInputStream());
                rsoos.writeObject(login);
                input = rsois.readObject();
                authenticated = !(input instanceof Exception) && authenticated;
                if (!authenticated) {
                    disconnect();
                } else {
                    if (gpsThread != null) {
                        gpsThread.interrupt();
                    }
                    if (listenThread != null) {
                        listenThread.interrupt();
                    }
                    gpsThread = new GPSThread();
                    gpsThread.start();
                }
            }
        } catch (SocketException e) {
            Log.w("ObjectStream", "Socket Closed");
        } catch (IOException e) {
            Log.w("ObjectStream", "IO Exceptions");
        } catch (ClassNotFoundException e) {
            Log.w("ObjectStream", "Object not serializable");
        }
    }

    private class GPSThread extends Thread {
        public GPSThread() {
            super("GPSThread");
        }

        public void run() {
            gpsThreading = true;
            try {
                while (gpsThreading) {
                    request(gpsQueue.take());
                }
            } catch (InterruptedException e) {

            }
        }
    }

    private class ListenThread extends Thread {
        public ListenThread() {
            super("ListenThread");
        }

        public void run() {
            try {
                while (authenticated && connect()) {
                    Object input = null;
                    lsoos.flush();
                    input = lsois.readObject();
                    if (input instanceof Route) {
                        hasNextRoute = true;
                        Log.e("CRASHING", "Size " + ((Route) input).route.size());
                        nextRoute = ((Route) input);
                    }
                    if (input instanceof CheckListPair) {
                        clp = (CheckListPair) input;
                    }
                    synchronized (this) {
                        notify();
                    }

                }
            } catch (SocketException e) {
                Log.w("ObjectStream", "Socket Closed");
                disconnect();
            } catch (IOException e) {
                if (!listenSocket.isInputShutdown()) {
                    Log.i("ObjectStream", "Shutdown");
                } else {
                    Log.w("ObjectStream", "IO Exceptions");
                }
            } catch (ClassNotFoundException e) {
                Log.w("ObjectStream", "Object not serializable");
            }
        }
    }

    private class RequestThread extends Thread {
        private Object input;
        private Object message;

        public RequestThread(Object message) {
            super("RequestThread");
            this.input = null;
            this.message = message;
        }

        public void run() {
            try {
                if (authenticated && connect()) {
                    rsoos.writeObject(message);
                    rsoos.flush();
                    input = rsois.readObject();
                }
            } catch (SocketException e) {
                Log.w("ObjectStream", "Socket Closed");
                input = null;
            } catch (IOException e) {
                Log.w("ObjectStream", "IO Exceptions");
                input = null;
            } catch (ClassNotFoundException e) {
                Log.w("ObjectStream", "Object not serializable");
                input = null;
            }
        }

        public Object getInput() {
            return input;
        }
    }
}
